<template>
  <div class="y-form">
    <el-form
      ref="form"
      :label-position="labelPosition"
      :label-width="labelWidth"
      :model="model"
      :disabled="readOnly"
      :size="size"
    >
      <el-row v-for="(item, index) of list" :key="index" :gutter="gutter">
        <el-col v-for="(it, i) of item.cols" :key="i" :span="getSpan(it.span)">
          <el-form-item :prop="it.prop" :rules="it.rules">
            <span slot="label" class="label">
              <span>{{ it.label }}</span>
              <el-tooltip
                style="margin-left: 5px"
                v-if="it.tooltip && it.tooltip.content"
                :effect="effect(it)"
                :placement="placement(it)"
              >
                <div class="y-form-tip-content" slot="content">
                  {{ it.tooltip.content }}
                </div>
                <i :class="it.icon || 'el-icon-info'"></i>
              </el-tooltip>
            </span>
            <component
              v-on="$listeners"
              @change="handleChange(it)"
              :modelValue="it.getModelValue()"
              @modelInput="it.setModelValue"
              :formModel="model"
              :item="it"
              :is="isComponent(it)"
            >
              <template slot-scope="{ self, model, formModel }">
                <slot :self="self" :model="model" :formModel="formModel"></slot>
              </template>
            </component>
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
  </div>
</template>

<script>
import { modules } from "./component";
export default {
  name: "y-form",
  props: {
    formConfig: {
      type: Object,
      default: () => {},
    },
    modelValue: {
      type: Object,
      default: () => {},
    },
    readOnly: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      initModelOrList: [],
    };
  },
  watch: {
    formConfig: {
      deep: true,
      immediate: true,
      handler(value) {
        // TODO console.log('检测到表单配置项发生了改变,开始进行初始化操作');
        this.initModel();
      },
    },
    model() {
      this.initModel();
    },
  },
  computed: {
    gutter() {
      return this.formConfig.gutter || 0;
    },
    rows() {
      return this.formConfig.rows || 4;
    },
    getSpan() {
      return (col) => {
        let r = 24 / this.rows;
        let result = col ? r * col : r * this.cols;
        return result;
      };
    },
    cols() {
      return this.formConfig.cols || 1;
    },
    size() {
      return this.formConfig.size || "small";
    },
    /**
     * 获取对应所需组件
     */
    isComponent() {
      return (item) => {
        let module = modules.find((c) =>
          c.includesComponent.includes(item.type)
        );
        return module ? module.component : "";
      };
    },
    labelPosition() {
      return this.formConfig.labelPosition || "right";
    },
    labelWidth() {
      return (this.formConfig.labelWidth || 100) + "px";
    },
    model() {
      return this.modelValue;
    },
    /**
     * 获取list的时候给所有list配置对应的属性
     * 安装需要分成对应的行列
     * TODO 这边 model  list 发现改变都会造成 触发 思路方案可以将list 中initModel初始化方法放在合适位置调用, 比如在初始化的时候手动触发, 目前放在计算属性中调用
     * @return {Array}
     */
    list() {
      let list =
          this.initModelOrList.filter(
            (item) => !item.visible || item.visible(this.model)
          ) || [],
        res = [],
        rows = 0;
      list.forEach((item) => {
        let span = this.getSpan(item.span);
        if (rows === 0 || rows < span) {
          rows = Number((24 - span).toFixed(1));
          res.push({
            cols: [item],
          });
        } else if (rows >= span) {
          rows = Number((rows - span).toFixed(1));
          res[res.length - 1].cols.push(item);
        }
      });
      return res;
    },
    // 文字提示
    effect() {
      return (item) => {
        return item.tooltip.effect || "dark";
      };
    },
    placement() {
      return (item) => {
        return item.tooltip.placement || "top";
      };
    },
  },
  methods: {
    /**
     * 每次配置项发生变化的时候重新将对应功能绑定至对应属性上
     * 放在对应属性上导致了闭包无法释放,但是这样做的好处简介使用逻辑清晰
     * @param {Array}
     */
    initModel() {
      // TODO console.log("开始检测是否有特殊key需要转换");
      // 将所有配置项生成对应的链式层级结构,绑定对应层级结构中响应式对象
      let result = this.formConfig.list.map((item) => {
        // 生成props数组方便给予后面使用  闭包
        let props = item.prop.split(".");
        // 生成model给予当前配置项绑定key使用  闭包
        let model = this.model;
        // 如果props中不是普通的单个层级,则需要将model对象重新绑定,取出对应的引用对象地址
        if (props.length > 1) {
          // TODO console.log("遇到特殊的值,开始进行初始化绑定", props);
          // 在model中生成并取出最终生成的对象地址
          model = this.applicationObjValue(
            this.model,
            [...props],
            item.defaultValue
          );
        } else if (!model[item.prop] && item.prop) {
          // TODO console.log("普通值并且有默认值,进行赋值操作", props);
          this.$set(model, item.prop, item.defaultValue || null);
        }
        let key = props[props.length - 1];
        return {
          // 原来的属性不变
          ...item,
          // 增加链式生成的数组方便后续的使用
          props,
          // 每一个配置项绑定的值  因为链式的原因所以没有进行v-model绑定再取值和设置值得时候进行特殊处理
          // 这里面的model已经相对应的处理过了
          getModelValue: () => {
            return model[key];
          },
          // 设置值
          setModelValue: (value) => {
            this.$set(model, key, value);
          },
        };
      });
      this.initModelOrList = result;
      // return result;
    },
    /**
     * 对于特殊的 . 链式绑定引用对象 同时检测是否有该属性没有则添加响应式属性
     * 因为双向绑定只能够对引用对象内部的值进行操作,而无法控制生成对象的长度,所以取出最里面所需要的引用对象
     * @param {Object} model
     * @param {Object}
     * @return {Object}
     */
    applicationObjValue(model, props, vlaue = undefined) {
      // 创建result与model对象地址相互牵引
      let newObject = model;
      // 每次循环根据props长度来生成对应的长度 保留props一位,用于最后的赋值
      while (props.length > 1) {
        // 每次进来去除props第一位同时取出
        let key = props.shift();
        // 如果当前对象中没有当前的key 则生成一个空的对象,同时因为对象地址相互引用关系,同时model上面也会生成对应属性
        if (!newObject[key]) {
          this.$set(newObject, key, {});
        }
        // 将已经和model绑定的对象地址赋值给result
        newObject = newObject[key];
      }
      // 最后利用$set将最后的key绑定到最深层的对象上
      this.$set(
        newObject,
        props[0],
        props[0] in newObject ? newObject[props[0]] : vlaue
      );
      // 抛出model子节点的对象地址,方便给当前配置项绑定
      return newObject;
    },
    /**
     * 表单改变事件
     * @param {Objec} props
     */
    handleChange(it) {
      this.$emit("formChange", {
        value: it.getModelValue(),
        model: this.model,
        options: it,
      });
    },
    /**
     * 表单重置
     */
    resetForm() {
      // 重置完成后 需重新初始表单
      this.$refs["form"].resetFields();
    },
    /**
     * 表单校验
     */
    validate() {
      return new Promise((resolve) => {
        this.$refs["form"].validate((valid, rulesFail) => {
          resolve({
            valid,
            rulesFail,
          });
        });
      });
    },
    checkForm() {},
  },
};
</script>

<style lang="scss" scoped>
.y-form {
  width: 100%;
  /deep/.el-form-item {
    margin: 0 !important;
    padding: 9px 0;
  }
  &-tip-content {
    white-space: pre-line;
  }
}
</style>